不管是哪種語言,變數都是重要的基礎概念。
理解程式中的型別選項與特性,才能更好的上手一門語言。
在討論型別之前,我們先從定義「變數」與「型別」開始。
「變數」可以抽象地理解為一個儲存值或物件的容器,實際上,它會指向某個記憶體位址,記憶體位址有著對應的值儲存。
「型別」則是規範變數所指向對象的行為或特性的分類。
JavaScript 是一個弱型別語言,也稱為動態語言。這意味著變數在宣告時賦予的型別能夠在執行時被更改。例如,某變數現在存的是字串,稍後可能會改為存入數字。
在 ECMAScript 規範中,JavaScript 定義了以下幾種「原始型別」(Primitive Type):
原始型別之外、其他型別皆屬於複合型別(Complex Type),常見的有以下幾種:
原始型別與複合型別的主要差異在於:
可變性:
在屬性和方法上,原始型別不可變,而複合型別是可變的。
let a = 'abc';
a.attr1 = 2;
console.log(a.attr1);//undefined
let b = {};
b.attr1 = 2;
console.log(b.attr1);//2
傳遞方式:
在網路上經常會看到「傳值」和「傳址」的討論 - 通常認為,原始型別是「傳值」,而複合型別是「傳址」。這裡我們先把名詞放一旁,先著重於行為的討論,透過下面的程式碼,我們能觀察到兩種不同的行為:
function change(a, b) {
a += 5;
b.val += 5;
}
let a = 10;
let b = { val: 10 };
console.log('before', a, b);
change(a, b);
console.log('after', a, b);
上面程式碼輸出結果會是:
"before", 10, { val: 10 }
"after", 10, { val: 15 }
這個例子展示了,當傳入的是「原始型別」時,函數內的操作不會影響外部的變數值;但對於「複合型別」,函數內的修改則會反映到外部變數。
先看原始型別的第一個例子(a),外部作用域的 a 是一個指向記憶體中存放數值 10 的指標,在 change 函式內部,a 則是一個新的變數(與外部的 a 無關),指向同一個初始值 10 的位址,但當執行 a += 5 時,JavaScript 創建了一個新的記憶體位置來存放 15,並將內部的 a 指向該位址,外部的 a 指向的位址並未被這一改動調整。
對於複合型別的 b,情況則不同。實際上傳入 change 內部時,是傳入了外部 b.val 指向的位址,也就是內部在修改值的時候,實際上是把傳入的位址指向了新的 15。因為內部與外部 b.val 指向的是同一記憶體位址。這就是為什麼結果中 b.val 變為 15 的原因。
如果這段概念有些難理解,可以嘗試將變數和記憶體位址關係畫成圖表,左邊是變數,右邊是記憶體位址,最右邊是儲存的值。畫出變量與記憶體位址之間的關係,應該會更清晰。
屬性和方法:
原始型別並不具備屬性和方法,而複合型別可以擁有多種屬性和方法。
可能有人會問:字串有長度屬性length,這樣還能算是沒有屬性嗎?
事實上,原始型別的字串在背後會自動轉換為一個 String 包裝物件。這個過程是由 JS 在背景處理的,當訪問 a.length 時,JS 會創建一個 String 物件來處理操作,處理結束後立即銷毀該物件。
let a = "abc";
console.log(a.length); // 3
console.log(a.toUpperCase()); // "ABC"
在這段程式碼中,總共會產生兩個 String 包裝物件,第二行和第三行各一次。
比較行為:
在進行比較時,原始型別比較的是值,而複合型別比較的是記憶體位址。
let a1 = 5;
let a2 = 5;
let b1 = { val: 5 };
let b2 = { val: 5 };
let b3 = b1;
console.log(a1 == a2); // true
console.log(b1 == b2); // false
console.log(b1 == b3); // true
字面上看起來雖然 b1 和 b2 擁有相同的屬性命名與屬性值,但實際上宣告的 val: 5 是各自儲存在不同記憶體位址。複合型別比較時以位址比較,所以才得到結果 false。但通過 = 的賦值,實際上是把 b3 也指向 b1 所指向的位址,兩者指向同一位址,因此得到結果 true。